import axios, {
    AxiosError,
    type AxiosInstance,
    type AxiosRequestConfig,
    type AxiosResponse,
    type InternalAxiosRequestConfig
} from "axios";

import type { RequestOptions, ResponseStructure } from "./type";

type typeStr =
    | "function"
    | "array"
    | "string"
    | "number"
    | "object"
    | "boolean"
    | "null"
    | "undefined"
    | "symbol"
    | "bigint";
/**
 * @description 判断数据类型
 * @param {Any} val 需要判断类型的数据
 * @return string (function | array | string | number | object | boolean | null | undefined | symbol)
 */
function getType(val: any): typeStr {
    if (val === null) return "null";
    if (typeof val !== "object") return typeof val;
    else return Object.prototype.toString.call(val).slice(8, -1).toLocaleLowerCase() as typeStr;
}
const isFunction = (val: any): val is Function => getType(val) === "function";
// const isObject = (val: any): val is object => getType(val) === "object";
// const isArray = (val: any): val is Array<any> => getType(val) === "array";
// const isString = (val: any): val is string => getType(val) === "string";
// const isNumber = (val: any): val is number => getType(val) === "number";
// const isBoolean = (val: any): val is boolean => getType(val) === "boolean";
// const isNull = (val: any): val is null => getType(val) === "null";
// const isUndefined = (val: any): val is undefined => getType(val) === "undefined";
// const isSymbol = (val: any): val is symbol => getType(val) === "symbol";
// const isBigInt = (val: any): val is bigint => getType(val) === "bigint";

// const ContentTypeEnum = {
//     // json
//     JSON: "application/json;charset=UTF-8",
//     // form-data   上传资源（图片，视频）
//     FORM_DATA: "multipart/form-data;charset=UTF-8"
// } as const;

const RequestMethodsEnum = {
    GET: "GET",
    POST: "POST",
    PUT: "PUT",
    DELETE: "DELETE",
    PATCH: "PATCH"
} as const;

export class HttpRequest {
    private axiosInstance: AxiosInstance;
    private readonly config: AxiosRequestConfig;
    private readonly options: RequestOptions; // 全局自定义请求配置
    private cancelerMap: Map<string, AbortController>;
    private requestSet: Set<string>;
    private debuger: boolean;

    constructor(config: AxiosRequestConfig) {
        this.config = config;
        this.options = config.requestOptions;
        this.debuger = config.debuger || false;
        this.cancelerMap = new Map<string, AbortController>();
        this.requestSet = new Set<string>();
        this.axiosInstance = axios.create(config);
        this.setupGlobalInterceptors();
    }

    /**
     * @description 设置全局拦截器
     */
    private setupGlobalInterceptors() {
        if (!this.config.axiosHooks) return;

        const { requestInterceptorsHook, requestInterceptorsCatchHook } = this.config.axiosHooks;

        this.axiosInstance.interceptors.request.use(
            config => {
                if (this.debuger) console.log(`%c ${config.method} ${config.url} 请求开始`, "color:skyblue");
                // 请求防抖
                if (this.options.debounceRequest) {
                    const requestKey = this.getRequestKey(config);
                    if (this.requestSet.has(requestKey)) {
                        if (this.debuger) console.log(`%c 请求 ${requestKey} 已在队列中，取消本次请求`, "color:red");
                        return Promise.reject(new Error("请求已在队列中，请稍后再试"));
                    }
                    this.requestSet.add(requestKey);
                }
                this.addCancelSignal(config); // 为所有请求添加取消请求的标记
                if (isFunction(requestInterceptorsHook)) {
                    config = requestInterceptorsHook(config) as InternalAxiosRequestConfig;
                }
                return config;
            },
            (err: Error) => {
                if (isFunction(requestInterceptorsCatchHook)) {
                    requestInterceptorsCatchHook(err);
                }
                return err;
            }
        );

        this.axiosInstance.interceptors.response.use(
            (response: AxiosResponse<ResponseStructure>) => {
                // 清除请求队列
                if (this.options.debounceRequest) this.requestSet.delete(this.getRequestKey(response.config));
                this.removeCancelSignal(response.config);
                return response;
            },
            (err: AxiosError) => {
                if (this.debuger) console.log(`%c response-interceptors-catc ---->`, "color:yellow", err);

                // 清除请求队列
                if (this.options.debounceRequest && err.config) this.requestSet.delete(this.getRequestKey(err.config));

                // 对于非请求取消所导致的 rejected 状态，手动取消该请求
                if (err.code !== AxiosError.ERR_CANCELED) {
                    err.config && this.removeCancelSignal(err.config);
                }

                // 处理网络错误导致的 rejected 状态，请求重试。
                // ECONNABORTED：连接被意外终止(如请求超时，手动取消等) ERR_NETWORK：网络连接失败(如DNS解析失败、断网、跨域)
                if (err.code === AxiosError.ECONNABORTED || err.code === AxiosError.ERR_NETWORK) {
                    return new Promise(resolve => setTimeout(resolve, 1000)).then(() => this.retryRequest(err));
                }
                return Promise.reject(err);
            }
        );
    }

    /**
     * 获取当前请求的唯一标识（它由请求的 URL 和参数拼接成）
     * @param config axios 请求配置
     * @returns {string} 当前请求的唯一标识
     */
    private getRequestKey(config: AxiosRequestConfig): string {
        const { url, method, params, data } = config;
        return [method, url, JSON.stringify(params), JSON.stringify(data)].join("&");
    }

    /**
     * @description 添加取消请求的标记，token 或 signal
     */
    private addCancelSignal(config: InternalAxiosRequestConfig) {
        if (!this.options.isOpenRetry) return;
        // if (config.cancelToken || config.signal) return;
        const requestKey = this.getRequestKey(config);
        if (!this.cancelerMap.has(requestKey)) {
            const controller = new AbortController();
            config.signal = controller.signal;
            this.cancelerMap.set(requestKey, controller);
        }
    }

    /**
     * @description 取消请求
     */
    private removeCancelSignal(config: InternalAxiosRequestConfig) {
        const requestKey = this.getRequestKey(config);
        if (this.cancelerMap.has(requestKey)) {
            const controller = this.cancelerMap.get(requestKey);
            controller && controller.abort();
            this.cancelerMap.delete(requestKey);
        }
    }

    /**
     * @description 请求重试，注：post 请求不重试
     */
    private retryRequest(error: AxiosError) {
        const config = error.config as AxiosRequestConfig;
        const { retryCount, isOpenRetry, debounceRequest } = config.requestOptions;
        if (!isOpenRetry || config.method?.toUpperCase() === RequestMethodsEnum.POST) {
            return Promise.reject(error);
        }
        config.curRetryCount = config.curRetryCount ?? 0;
        if (config.curRetryCount === 0 && this.debuger) console.log(`%c 开始 ${config.url} 请求重试`, "color:red");
        if (config.curRetryCount >= retryCount) {
            if (this.debuger) console.log("%c 请求重试次数达到上限", "color:red");
            return Promise.reject(error);
        }
        config.curRetryCount++;
        if (debounceRequest) this.requestSet.delete(this.getRequestKey(config));
        if (this.debuger) console.log(`%c 开始第 ${config.curRetryCount} 次请求重试`, "color:green");
        return this.axiosInstance.request(config);
    }

    /**
     * @description 获取axios实例
     */
    getAxiosInstance() {
        return this.axiosInstance;
    }

    /**
     * @description 通用请求函数
     * @param config axios请求配置
     * @param options 自定义请求配置
     * @returns Promise
     * 泛型参数说明：
     * - TResData: 接口响应数据中data字段的类型（注：不是axios原生响应对象中的data字段，res.data.data）
     * - TReqBodyData: 请求体数据类型
     * - TResStructure: 接口响应数据结构类型，即axios原生响应对象中的data字段类型，res.data。默认 ResStructure，
     */
    private request<
        TResData = any,
        TReqBodyData = any,
        TResStructure = ResponseStructure<TResData> // 约束泛型继承默认结构，避免泛型层级太深无法识别
    >(config: Partial<AxiosRequestConfig>, options?: Partial<RequestOptions>): Promise<TResStructure> {
        const opt: RequestOptions = { ...this.options, ...options }; // 合并具体请求与全局请求的请求选项配置
        const axioxConfig: AxiosRequestConfig = {
            ...this.config,
            ...config,
            requestOptions: opt
        };
        const { responseInterceptorsHook, responseInterceptorsCatchHook } = axioxConfig.axiosHooks || {};

        const { urlPrefix, isExtractAPIDataField, isReturnNativeResponse } = opt;
        // 拼接请求前缀如api
        if (urlPrefix) axioxConfig.url = `${urlPrefix}${config.url}`;

        return new Promise((resolve, reject) => {
            // request 三个泛型依次为：接口响应结构类型、axios原生响应对象类型、请求体结构类型
            this.axiosInstance
                .request<TResStructure, AxiosResponse<TResStructure, TReqBodyData>, TReqBodyData>(axioxConfig)
                .then(async res => {
                    // 注意：这里是最晚才执行的，前面会先经过各个响应拦截器。因此 res 的值不确定，有三种情况：
                    // 1. axios 原生响应对象 AxiosResponse
                    // 2. 接口返回的对象：AxiosResponse.data
                    // 3. 提取接口对象中的data字段：AxiosResponse.data.data
                    // 因此，限制响应拦截hook的返回值必须是原生 axios 响应对象 AxiosResponse，然后在此处完成三种情况的处理。
                    // 默认情况下是第二种情况，因此断言 res 为 TResStructure 类型。
                    // 如果是其他情况，则因断言错误，res 的提示是错的，要注意。

                    try {
                        if (isFunction(responseInterceptorsHook))
                            res = (await responseInterceptorsHook(res as any)) as AxiosResponse<
                                TResStructure,
                                TReqBodyData
                            >;
                    } catch (error) {
                        // 捕获响应拦截hook抛出的reject状态。一般为业务错误的情况下才抛出。
                        // 继续向上抛出错误，让下方的 catch 捕获错误。
                        return Promise.reject(error);
                    }

                    let result;
                    if (isExtractAPIDataField)
                        result = (res.data as any)?.data as unknown as TResStructure; // 是否提取接口响应对象中的data字段返回。
                    else if (isReturnNativeResponse)
                        result = res as unknown as TResStructure; // 是否返回 axios 原生的响应对象，当需要获取响应头及其他数据时可使用
                    else result = res.data as unknown as TResStructure;

                    if (this.debuger)
                        console.log(
                            `%c ${axioxConfig.baseURL}${axioxConfig.url} 请求成功, 位置: axios.ts request 成功回调`,
                            "color:green",
                            result
                        );

                    resolve(result);
                })
                .catch(err => {
                    if (this.debuger)
                        console.log(
                            `%c ${axioxConfig.baseURL}${axioxConfig.url} 请求失败, 位置: axios.ts request 失败回调`,
                            "color:red",
                            err
                        );

                    if (!err) err = new Error("请求失败"); // 防止空错误
                    // 错误拦截hook，此处为最后的错误处理，可进行错误消息toast提示。
                    if (isFunction(responseInterceptorsCatchHook)) responseInterceptorsCatchHook(err);
                    reject(err);
                });
        });
    }

    /**
     * @description get请求
     * @param config axios请求配置
     * @param options 自定义请求配置
     * @returns Promise
     * 泛型参数说明：
     * - T: 默认接口响应结构 ResStructure 中 data 字段的类型
     * - D: 请求体数据类型
     * - R: 接口响应数据结构类型，即axios原生响应对象中的data字段类型，res.data。默认 ResStructure。
     *      若设置该泛型则会覆盖默认的ResStructure，同时泛型 T 失效。
     */
    get<T = any, D = any, R = ResponseStructure<T>>(
        config: Partial<AxiosRequestConfig>,
        options?: Partial<RequestOptions>
    ) {
        return this.request<T, D, R>({ ...config, method: RequestMethodsEnum.GET }, options);
    }

    /**
     * @description post请求
     * @param config axios请求配置
     * @param options 自定义请求配置
     * @returns Promise
     * 泛型参数说明：
     * - T: 默认接口响应结构 ResStructure 中 data 字段的类型
     * - D: 请求体数据类型
     * - R: 接口响应数据结构类型，即axios原生响应对象中的data字段类型，res.data。默认 ResStructure。
     *      若设置该泛型则会覆盖默认的ResStructure，同时泛型 T 失效。
     */
    post<T = any, D = any, R = ResponseStructure<T>>(
        config: Partial<AxiosRequestConfig>,
        options?: Partial<RequestOptions>
    ) {
        return this.request<T, D, R>({ ...config, method: RequestMethodsEnum.POST }, options);
    }

    /**
     * @description put请求 （全量更新字段，要求幂等）
     * @param config axios请求配置
     * @param options 自定义请求配置
     * @returns Promise
     * 泛型参数说明：
     * - T: 默认接口响应结构 ResStructure 中 data 字段的类型
     * - D: 请求体数据类型
     * - R: 接口响应数据结构类型，即axios原生响应对象中的data字段类型，res.data。默认 ResStructure。
     *      若设置该泛型则会覆盖默认的ResStructure，同时泛型 T 失效。
     */
    put<T = any, D = any, R = ResponseStructure<T>>(
        config: Partial<AxiosRequestConfig>,
        options?: Partial<RequestOptions>
    ) {
        return this.request<T, D, R>({ ...config, method: RequestMethodsEnum.PUT }, options);
    }

    /**
     * @description delete请求
     * @param config axios请求配置
     * @param options 自定义请求配置
     * @returns Promise
     * 泛型参数说明：
     * - T: 默认接口响应结构 ResStructure 中 data 字段的类型
     * - D: 请求体数据类型
     * - R: 接口响应数据结构类型，即 axios 原生响应对象中的 data 字段类型，res.data。默认 ResStructure。
     *      若设置该泛型则会覆盖默认的 ResStructure，同时泛型 T 失效。
     */
    delete<T = any, D = any, R = ResponseStructure<T>>(
        config: Partial<AxiosRequestConfig>,
        options?: Partial<RequestOptions>
    ) {
        return this.request<T, D, R>({ ...config, method: RequestMethodsEnum.DELETE }, options);
    }

    /**
     * @description patch请求 (局部更新字段，幂等取决于具体实现)
     * @param config axios请求配置
     * @param options 自定义请求配置
     * @returns Promise
     * 泛型参数说明：
     * - T: 默认接口响应结构 ResStructure 中 data 字段的类型
     * - D: 请求体数据类型
     * - R: 接口响应数据结构类型，即axios原生响应对象中的data字段类型，res.data。默认 ResStructure。
     *      若设置该泛型则会覆盖默认的ResStructure，同时泛型 T 失效。
     */
    patch<T = any, D = any, R = ResponseStructure<T>>(
        config: Partial<AxiosRequestConfig>,
        options?: Partial<RequestOptions>
    ) {
        return this.request<T, D, R>({ ...config, method: RequestMethodsEnum.PATCH }, options);
    }
}
